﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PServiceBus.Core.Interface;
using PServiceBus.Core.Runtime.Transports;
using System.Net;
using System.IO;
using System.Runtime.Serialization;
using System.ComponentModel.Composition;
using PServiceBus.Core.Runtime;
using System.Reflection;
using PServiceBus.Core.Provider;
using Phoenix.ReflectionExtensions;
using System.Collections;
using PServiceBus.Core.Runtime.Messages;
using System.Xml;

namespace PServiceBus.Transports {
    [Serializable]
    [DataContract]
    [Export(typeof(ITransport))]
    public class WebServiceTransport : ITransport {

        [DataMember]
        public string Namespace { get; set; }

        [DataMember]
        public string Url { get; set; }

        [DataMember]
        public string Method { get; set; }

        /// <summary>
        /// Key => parameter name, Value => Property Name of message to map to web service parameter
        /// </summary>
        [DataMember]
        public Dictionary<string,string> MethodParameters { get; set; }

        #region ITransport Members
        [DataMember]
        public TransportFormat Format { get; set; }

        [MessageShark.MessageSharkIgnore]
        private MessageMapper _messageMapper;

        public TransportResponse Send(string message, Dictionary<string, object> messageStructure) {
            var tranResponse = TransportResponse.New<WebServiceTransport>();
            try {
                Namespace = Namespace.Replace("\\", "/");
                Namespace = Namespace + (Namespace.EndsWith("/") ? string.Empty : "/");
                _messageMapper = MessageMapper.Map(message, messageStructure, excludeXmlItemsAttribute: true);
                var methodSoapXml = ExtractSoapMethodXmlConstruct(Method, Url);
                foreach (var msg in _messageMapper.Tokens) InvokeWS(msg, methodSoapXml);
                tranResponse.Success = true;
            }
            catch (Exception ex) {
                tranResponse.Message = ex.ToString();
            }
            return tranResponse;
        }

        private void InvokeWS(MessageMapperToken message, string methodSoapXml) {

            var soapBody =
                    String.Join("\n", MethodParameters.Select(param =>
                        String.Format("<{0}>{1}</{0}>", param.Key, message[param.Value])));

            soapBody = MergeSoapXml(soapBody, methodSoapXml);

            var client = WebRequest.Create(Url) as HttpWebRequest;
            client.Headers.Add("SOAPAction", String.Format("\"{0}{1}\"", Namespace, Method));
            client.ContentType = "text/xml; charset=utf-8";
            client.Method = "POST";
            client.Accept = "text/xml";
            var soapXml = String.Format(@"<?xml version=""1.0"" encoding=""utf-8""?>
                        <soap:Envelope xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"" 
                            xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" 
                            xmlns:xsd=""http://www.w3.org/2001/XMLSchema"">
                            <soap:Body>
                                <{1} xmlns=""{0}"">
                                    {2}
                                </{1}>
                            </soap:Body>
                        </soap:Envelope>", Namespace, Method, soapBody);

            SendSoapEnvelope(client, soapXml);
            client.Abort();
        }

        private void SendSoapEnvelope(HttpWebRequest client, string soapXml) {
            
            using (var requestStream = client.GetRequestStream()) 
                using (var sw = new StreamWriter(requestStream))
                    sw.Write(soapXml);
            

            using (var response = client.GetResponse()) 
                using (var sr = new StreamReader(response.GetResponseStream()))
                    sr.ReadToEnd();
        }

        #endregion

        #region WebService Helpers

        static XmlNodeList GetChildNodes(string xml) {
            var docXml = new XmlDocument();
            docXml.LoadXml("<XmlObject>" + xml + "</XmlObject>");
            return docXml.ChildNodes[0].ChildNodes;
        }

        static string MergeSoapXml(string sourceXml, string targetXml) {
            var validXml = sourceXml;
            var mapping = new Dictionary<int, string>();
            var dupList = new HashSet<string>();
            var counter = 0;
            var childNodes = GetChildNodes(sourceXml);
            foreach (XmlNode node in childNodes)
                BuildXmlMapping(node, mapping, ref counter);

            counter = 0;

            var targetChildNodes = GetChildNodes(targetXml);

            foreach (XmlNode node in targetChildNodes)
                MapXmlByMappingIndex(ref validXml, node, dupList, mapping, ref counter);

            return validXml;
        }

        static void MapXmlByMappingIndex(ref string xml, XmlNode node, HashSet<string> dupList, Dictionary<int, string> mapping, ref int counter) {
            if (node is XmlText || node is XmlCharacterData) return;
            if (!dupList.Contains(node.Name)) {
                dupList.Add(node.Name);
                ++counter;
            }

            if (mapping.ContainsKey(counter))
                xml = xml.Replace(mapping[counter], node.Name);

            var childNodes = node.ChildNodes;
            foreach (XmlNode xmlNode in childNodes)
                MapXmlByMappingIndex(ref xml, xmlNode, dupList, mapping, ref counter);
        }

        static void BuildXmlMapping(XmlNode node, Dictionary<int, string> mapping, ref int counter) {
            if (node is XmlText || node is XmlCharacterData) return;
            if (!mapping.ContainsValue(node.Name)) mapping[++counter] = node.Name;
            var childNodes = node.ChildNodes;
            foreach (XmlNode xmlNode in childNodes)
                BuildXmlMapping(xmlNode, mapping, ref counter);
        }

        static string ExtractSoapMethodXmlConstruct(string methodName, string url) {

            url += url.EndsWith("?wsdl") ? string.Empty : "?wsdl";
            var extractedXml = new StringBuilder();
            var client = WebRequest.Create(url) as HttpWebRequest;
            var xml = string.Empty;

            using (var response = client.GetResponse())
            using (var sr = new StreamReader(response.GetResponseStream()))
                xml = sr.ReadToEnd();

            var doc = new XmlDocument();

            doc.LoadXml(xml);

            var element =
                doc.GetElementsByTagName("s:element")
                .Cast<XmlElement>()
                .Where(node => node.Attributes["name"].Value == methodName)
                .FirstOrDefault();

            if (element == null) return extractedXml.ToString();
            var elements = element.GetElementsByTagName("s:element");

            foreach (XmlElement elem in elements) {
                var elementType = elem.Attributes["type"].Value;
                extractedXml.Append(GetSoapMethodXml(elem, elementType,
                isFirst: true));
            }

            return extractedXml.ToString();
        }

        static string GetSoapMethodXml(XmlElement element, string parentType,
            bool isFirst = false) {
            var doc = element.OwnerDocument;
            var xml = new StringBuilder();

            var elementName = element.Attributes["name"].Value;
            var elementType = element.Attributes["type"].Value;
            var isElementComplex = elementType.StartsWith("tns:");
            var isElementArray =
                element.Attributes["maxOccurs"].Value == "unbounded";

            var elementTypeName =
                isElementComplex ? elementType.Split(':')[1] : string.Empty;

            var complexElement =
                        !String.IsNullOrWhiteSpace(elementTypeName) ?
                        doc.GetElementsByTagName("s:complexType")
                        .Cast<XmlElement>()
                        .Where(node =>
                            node.Attributes.Count > 0 &&
                            node.Attributes["name"].Value
                            == elementTypeName)
                            .FirstOrDefault() : null;

            var elements = complexElement != null ?
                complexElement.GetElementsByTagName("s:element")
                : null;

            if (elements == null || isElementArray
                || (elementType == parentType && !isFirst))
                return String.Format("<{0}/>", elementName);


            xml.AppendFormat("<{0}>", elementName);

            foreach (XmlElement elem in elements)
                xml.Append(GetSoapMethodXml(elem, parentType));

            xml.AppendFormat("</{0}>", elementName);

            return xml.ToString();
        }

        #endregion
    }
}
